Topic 3 Multithreading (3)

Leedehai
Firday, May 5, 2017
Monday, May 8, 2017

When coding with threads, you need to ensure:
- that there are no possibility of race conditions, and...
- that there's no possibility of deadlock.

3.5 The threat of deadlock: dining philosophers problem (KOB)

mutex is not the panacea of all synchronization problems in multithreading. In fact, mutex can solve the problem of race conditions (as in 3.4), but not the one we are going to describe below.

In computer science, the dining philosophers problem is an example problem often used in concurrent algorithm design to illustrate synchronization issues and techniques for resolving them. It was originally formulated in 1965 by Edsger Dijkstra as an exam problem.

.─. THE SETUP: ( ) Plato - A circular table, `─' - N philosophers (N >= 2), ┌──────────────────────────┐ - N forks, each placed between │ \ ┌────┐ / │ two adjacent philosophers, │ \ │bowl│ / │ - M meals for each philosopher, │ └────┘ │ - Each philosopher needs 2 │ ┌──────────┐ │ adjacent forks to eat .─. │┌────┐ │ │ ┌────┐│ .─. spaghetti (yeah, weirdos), ( ) ││bowl│ │spaghetti │ │bowl││ ( ) otherwise he philosophizes. `─' │└────┘ │ │ └────┘│ `─' Hegel │ └──────────┘ │ Descartes │ ┌────┐ │ │ / │bowl│ \ │ THE PROBLEM: │ / └────┘ \ │ How should each philosopher └──────────────────────────┘ behave so no one starves .─. i.e. each one is able to N = 4 here, but ( ) Rousseau eat M times? We use N = 5 below `─'

3.5.1 Version 1: deadlock happens with nonzero probability

Let's look at the code first.

/* Verion 1: deadlock may happen */ #include ... using namespace std; static const unsigned int kNumPhilosophers = 5; static const unsigned int kNumForks = kNumPhilosophers; static const unsigned int kNumMeals = 3; /* forks modeled as mutexes */ static mutex forks[kNumForks]; static void think(unsigned int id) { cout << oslock << id << " starts thinking." << endl << osunlock; sleep_for(getThinkTime()/* random number, not important here */); cout << oslock << id << " all done thinking. " << endl << osunlock; } static void eat(unsigned int id) { unsigned int left = id; unsigned int right = (id + 1) % kNumForks; forks[left].lock(); /* lock, if already locked, then wait */ forks[right].lock(); /* lock, if already locked, then wait */ cout << oslock << id << " starts eating." << endl << osunlock; sleep_for(getEatTime()); cout << oslock << id << " done eating." << endl << osunlock; forks[left].unlock(); /* unlock */ forks[right].unlock(); /* unlock */ } static void philosopher(unsigned int id) { for (unsigned int i = 0; i < kNumMeals; i++) { think(id); eat(id); } } int main(int argc, const char *argv[]) { thread philosophers[kNumPhilosophers]; for (unsigned int i = 0; i < kNumPhilosophers; i++) philosophers[i] = thread(philosopher, i); for (thread& p: philosophers) p.join(); return 0; }

A deadlock is a situation in which two or more competing actions are each waiting for the other to finish, and thus neither ever does.
If a race condition happens, the program can still proceed (though potentially with erroneous results), but if a deadlock happens, the program falls into a never-ending wait, like an obscure while-true loop.

3.5.2 Version 2: permission slips, limit the number of bidding threads

/* Version 2: no bug, but has busy-waiting */ #include ... using namespace std; static const unsigned int kNumPhilosophers = 5; static const unsigned int kNumForks = kNumPhilosophers; static const unsigned int kNumMeals = 3; /* forks modeled as mutexes -- to solve the race condition */ static mutex forks[kNumForks]; /* impose limit on # bidding threads -- to solve the deadlock */ static unsigned int numAllowed = kNumPhilosophers - 1; /* the lock partnered with numAllowd */ static mutex numAllowedLock; static void think(unsigned int id) { /* same as in 3.5.1 */ } /* wait to get a permission slip to participate in the bid for forks */ static void waitForPermission() { while (true) { numAllowedLock.lock(); if (numAllowed > 0) break; numAllowedLock.unlock(); sleep_for(10); } numAllowed--; numAllowedLock.unlock(); } /* give back the permission slip to the pool so others can participate */ static void grantPermission() { numAllowedLock.lock(); numAllowed++; numAllowedLock.unlock(); } static void eat(unsigned int id) { unsigned int left = id; unsigned int right = (id + 1) % kNumForks; waitForPermission(); /* wait for permission until get one */ forks[left].lock(); /* may wait here */ forks[right].lock(); /* may wait here */ cout << oslock << id << " starts eating." << endl << osunlock; sleep_for(getEatTime()); cout << oslock << id << " all done eating." << endl << osunlock; grantPermission(); /* give back the permission to the pool */ forks[left].unlock(); forks[right].unlock(); } static void philosopher(unsigned int id) { /* same as in 3.5.1 */ } int main(int argc, const char *argv[]) { /* same as in 3.5.1 */ }

3.5.3 Version 3: improve the solution - no busy-waiting

/* Version 3: no bug, no busy-waiting */ #include ... using namespace std; static const unsigned int kNumPhilosophers = 5; static const unsigned int kNumForks = kNumPhilosophers; static const unsigned int kNumMeals = 3; /* forks modeled as mutexes -- to solve the race condition */ static mutex forks[kNumForks]; /* impose limit on # bidding threads -- to solve the deadlock */ static unsigned int numAllowed = kNumPhilosophers - 1; /* the lock meant to protect numAllowed against ++, -- */ static mutex numAllowedLock; /* to solve the busy-wait introduced in 3.5.2 */ static condition_variable_any cv; static void think(unsigned int id) { /* same as in 3.5.1 */ } /* wait to get a permission slip to participate in the bid for forks */ static void waitForPermission() { lock_guard<mutex> lg(numAllowedLock); /* test: if condition is met: proceed otherwise: release lock, sleep & wait notified: re-acquire lock, re-test */ cv.wait(numAllowedLock, []{ return numAllowed > 0; }); numAllowed--; } /* give back the permission slip to the pool so others can participate */ static void grantPermission() { lock_guard<mutex> lg(numAllowedLock); numAllowed++; if (numAllowed == 1) { /* if numAllowed goes from 0 to 1 */ /* notify cv to re-test the sleeping threads' condition */ cv.notify_all(); } } static void eat(unsigned int id) { /* same as in 3.5.2 */ } static void philosopher(unsigned int id) { /* same as in 3.5.1 and 3.5.2 */ /* 1. wait for permission until get one, 2. lock both forks (may wait here), 3. enjoy the spaghetti, 4. give back the permission to the pool, 5. unlock both forks */ } int main(int argc, const char *argv[]) { /* same as in 3.5.1 and 3.5.2 */ }
First call (lock should be locked already) │ ▼ ┌───────────────┐ ├◀─success─┤re-acquire lock◀────┐ sleep: blocked (off CPU) │ └──┬───────▲────┘ │ & wait for an event ┌────────▼───────┐ fail lock │ of interest │ condition? │ │ available │ └────────┬───────┘ ┌─▼───────┴─┐ │ ├──false──┐ │ sleep │ │ │ │ └───────────┘ │ │ ┌──────▼─────┐ │ true │release lock│ │ │ └──────┬─────┘ notification <= notify_all() │ ┌──────▼─────┐ │ │ │ sleep │───────────────┘ │ └────────────┘ ┌─────────▼─────────┐ │ continue to hold │ │ lock & proceed │ FLOWCHART: └─────────┬─────────┘ void wait(Lock &lock, Predicate condition); ▼
EOF